【算法学习】字符串Hash入门

字符串Hash入门

字符串Hash可以通俗的理解为,把一个字符串转换为一个整数

如果我们通过某种方法,将字符串转换为一个整数,就可以便的确定某个字符串是否重复出现过,这是最简单的字符串Hash应用情景了。

当然也不难想到,如果有不同的两个字符串同时Hash到一个整数,这样就比较麻烦了。我们希望这个映射是一个单射,所以问题就是如何构造这个Hash函数,使得他们成为一个单射。不用担心,接下来的内容正要讲解。

Hash方法

给定一个字符串 S = s 1 s 2 s 3 . . s n S = s_1s_2s_3..s_n S=s1s2s3..sn,对字母x,我们规定 i d x ( x ) = x − ′ a ′ + 1 idx(x) = x - 'a' +1 idx(x)=xa+1。 (当然也可以直接用 s i s_i si A S C I I ASCII ASCII值)

自然溢出方法

Hash公式

unsigned long long Hash[n]

h a s h [ i ] = h a s h [ i − 1 ] ∗ p + i d ( s [ i ] ) hash[i] = hash[i-1] * p + id(s[i]) hash[i]=hash[i1]p+id(s[i])

利用unsigned long long的范围自然溢出,相当于自动对 2 64 − 1 2^{64} -1 2641取模

单Hash方法

Hash公式

h a s h [ i ] = ( h a s h [ i − 1 ] ) ∗ p + i d x ( s [ i ] )   %   m o d hash[i] = (hash[i-1]) * p + idx(s[i]) \ \% \ mod hash[i]=(hash[i1])p+idx(s[i]) % mod

其中 p p p m o d mod mod均为质数,且有 p < m o d p < mod p<mod

对于此种Hash方法,将p和mod尽量取大即可,这种情况下,冲突的概率是很低的。

举例

如取 p = 13 , m o d = 101 p = 13, mod = 101 p=13,mod=101,对字符串 a b c abc abc进行Hash
hash[0] = 1
hash[1] = (hash[0] * 13 + 2) % 101 = 15
hash[2] = (hash[1] * 13 + 3) % 101 = 97

这样,我们就认为字符串 a b c abc abc当做97,即97就是 a b c abc abc 的hash值。

双Hash方法

将一个字符串用不同的 m o d mod mod hash两次,将这两个结果用一个二元组表示,作为Hash结果。

Hash公式

h a s h 1 [ i ] = ( h a s h 1 [ i − 1 ] ) ∗ p + i d x ( s [ i ] )   %   m o d 1 hash1[i] = (hash1[i-1]) * p + idx(s[i]) \ \% \ mod1 hash1[i]=(hash1[i1])p+idx(s[i]) % mod1

h a s h 2 [ i ] = ( h a s h 2 [ i − 1 ] ) ∗ p + i d x ( s [ i ] )   %   m o d 2 hash2[i] = (hash2[i-1]) * p + idx(s[i]) \ \% \ mod2 hash2[i]=(hash2[i1])p+idx(s[i]) % mod2

hash结果为 < h a s h 1 [ n ] , h a s h 2 [ n ] > <hash1[n],hash2[n]> <hash1[n],hash2[n]>

这种Hash很安全。

获取子串的Hash

如果我们求出一个串的Hash,就可以 O ( 1 ) O(1) O(1)求解其子串的Hash值。
我们先以一个具体的例子来理解。

例子

假设有一 ∣ S ∣ = 5 |S| = 5 S=5的字符串,设 S i S_i Si为第 i i i个字符,其中 1 ≤ i ≤ 5 1\le i \le 5 1i5

根据定义分别求出 h a s h [ i ] hash[i] hash[i]

h a s h [ 1 ] = s 1 hash[1] = s_1 hash[1]=s1
h a s h [ 2 ] = s 1 ∗ p + s 2 hash[2] = s_1 * p +s_2 hash[2]=s1p+s2
h a s h [ 3 ] = s 1 ∗ p 2 + s 2 ∗ p + s 3 hash[3] = s_1 * p ^ 2 +s_2 * p + s_3 hash[3]=s1p2+s2p+s3
h a s h [ 4 ] = s 1 ∗ p 3 + s 2 ∗ p 2 + s 3 ∗ p + s 4 hash[4] = s_1 * p^3 + s_2 * p^2 + s_3 * p + s_4 hash[4]=s1p3+s2p2+s3p+s4
h a s h [ 5 ] = s 1 ∗ p 4 + s 2 ∗ p 3 + s 3 ∗ p 2 + s 4 ∗ p + s 5 hash[5] = s_1 * p^4 + s_2 * p^3 + s_3 * p^2 + s_4 * p + s_5 hash[5]=s1p4+s2p3+s3p2+s4p+s5

现在我们想求 s 3 s 4 s_3s_4 s3s4的hash值,不难得出为 s 3 ∗ p + s 4 s_3 * p +s_4 s3p+s4,并且从上面观察,如果看 h a s h [ 4 ] − h a s h [ 2 ] hash[4] - hash[2] hash[4]hash[2]并将结果种带有 s 1 , s 2 s_1,s_2 s1,s2系数的项全部消掉,就是所求。但是由于 p p p的阶数,不能直接消掉,所以问题就转化成,将 h a s h [ 2 ] hash[2] hash[2]乘一个关于 p p p的系数,在做差的时候将多余项消除,从而得到结果。

不难发现,对应项系数只差一个 p 2 p^2 p2,而4 - 3 + 1 = 2(待求hash子串下标相减再加一),这样就不难推导出来此例题的求解式子。

h a s h [ 4 ] − h a s h [ 2 ] ∗ p 4 − 3 + 1 hash[4] - hash[2] * p ^{ 4 - 3 + 1} hash[4]hash[2]p43+1

至此,通过对上例的归纳,可以得出如下的公式。

公式

若已知一个 ∣ S ∣ = n |S| = n S=n的字符串的hash值, h a s h [ i ] , 1 ≤ i ≤ n hash[i], 1\le i \le n hash[i],1in,其子串 s l . . s r , 1 ≤ l ≤ r ≤ n s_l..s_r, 1 \le l \le r \le n sl..sr,1lrn对应的hash值为:

h a s h = h a s h [ r ] − h a s h [ l − 1 ] ∗ p r − l + 1 hash = hash[r] - hash[l-1] * p^{r - l + 1} hash=hash[r]hash[l1]prl+1

考虑到 h a s h [ i ] hash[i] hash[i]每次对 p p p取模,进一步得到下面的式子:

h a s h = ( h a s h [ r ] − h a s h [ l − 1 ] ∗ p r − l + 1 ) % M O D hash = (hash[r] - hash[l-1] * p^{r - l + 1}) \% MOD hash=(hash[r]hash[l1]prl+1)%MOD

看起来这个式子人畜无害,但是对于取模运算要谨慎再谨慎,注意到括号里面是减法,即有可能是负数,故做如下的修正:

h a s h = ( ( h a s h [ r ] − h a s h [ l − 1 ] ∗ p r − l + 1 ) % M O D + M O D ) % M O D hash = ((hash[r] - hash[l-1] * p^{r - l + 1}) \% MOD + MOD ) \% MOD hash=((hash[r]hash[l1]prl+1)%MOD+MOD)%MOD

至此得到求子串hash值公式。

值得一提的是,如果需要反复对子串求解hash值,预处理 p p p n n n次方效果更佳。

字符串Hash的应用

题型一

描述

问题:给两个字符串S1,S2,求S2是否是S1的子串,并求S2在S1中出现的次数

数据范围:1=<|S1|,|S2|<=10000

解法

求出S1和S2的Hash值,并且 n 2 n^2 n2的求解出S1所有子串的Hash值,放入map中,查询即可。复杂度 n 2 l o g n n^2logn n2logn

题型二

描述

问题:给N个单词串,和一个文章串,求每个单词串是否是文章串的子串,并求每个单词在文章中出现的次数。

数据范围:文章串长度:[1,105],N个单词串总长:[1,106]

解法

设单词串总长为 ∣ S ∣ |S| S,文章串总长为 ∣ A ∣ |A| A

此题和第一题做法相同。复杂度 ∣ A ∣ 2 l o g ∣ A ∣ + ∣ S ∣ |A|^2log|A|+|S| A2logA+S

题型三

描述

问题:给两个字符串S1,S2,求它们的最长公共子串的长度。

数据范围:1=<|S1|,|S2|<=10^5

解法

将S1的每一个子串都hash成一个整数

将S2的每一个子串都hash成一个整数

两堆整数,相同的配对,并且找到所表示的字符串长度最大的即可。

复杂度: O ( ∣ S 1 ∣ 2 + ∣ S 2 ∣ 2 ) O(|S1|^2+|S2|^2) O(S12+S22)

PS:为觉得开数组不保险,所以上面的题一和题二都用的map存,这里我也不知道能不能实现 O ( 1 ) O(1) O(1)的存储和查询。

题型四

描述

问题:给一个字符串S,求S的最长回文子串。

比如abcbbabbc的最长回文子串是cbbabbc,bbabb也是回文串,但不是最长的

数据范围: 1=<|S|<=10^5

解法

先求子串长度位奇数的,再求偶数的。枚举回文子串的中心位置,然后二分子串的长度,直到找到一个该位置的最长回文子串,不断维护长度最大值即可。

复杂度: O ( ∣ S ∣ ∗ l o g ∣ S ∣ ) O(|S|*log|S|) O(SlogS)

Hash素数的选取

为了防止冲突,要选择合适的素数,像1e9+7,1e9+9的一些素数,出题人一般会卡一下下,所以尽量选择其他的素数,防止被卡。下面是一些可供选择的素数。
上界和下界指的是离素数最近的 2 n 2^n 2n的值。

lwr(下界)upr(上界)%err(冲突率)prime(素数)
2 5 2^5 25 2 6 2^6 2610.41666753
2 6 2^6 26 2 7 2^7 271.041667097
2 7 2^7 27 2 8 2^8 280.520833193
2 8 2^8 28 2 9 2^9 291.302083389
2 9 2^9 29 2 10 2^{10} 2100.130208769
2 10 2^{10} 210 2 11 2^{11} 2110.4557291543
2 11 2^{11} 211 2 12 2^{12} 2120.2278653079
2 12 2^{12} 212 2 13 2^{13} 2130.1139326151
2 13 2^{13} 213 2 14 2^{14} 2140.00813812289
2 14 2^{14} 214 2 15 2^{15} 2150.06917324593
2 15 2^{15} 215 2 16 2^{16} 2160.01017349157
2 16 2^{16} 216 2 17 2^{17} 2170.01322498317
2 17 2^{17} 217 2 18 2^{18} 2180.002543196613
2 18 2^{18} 218 2 19 2^{19} 2190.006358393241
2 19 2^{19} 219 2 20 2^{20} 2200.000128786433
2 20 2^{20} 220 2 21 2^{21} 2210.0003181572869
2 21 2^{21} 221 2 22 2^{22} 2220.0003503145739
2 22 2^{22} 222 2 23 2^{23} 2230.0002076291469
2 23 2^{23} 223 2 24 2^{24} 2240.00004012582917
2 24 2^{24} 224 2 25 2^{25} 2250.00007525165843
2 25 2^{25} 225 2 26 2^{26} 2260.00001050331653
2 26 2^{26} 226 2 27 2^{27} 2270.000023100663319
2 27 2^{27} 227 2 28 2^{28} 2280.000009201326611
2 28 2^{28} 228 2 29 2^{29} 2290.000001402653189
2 29 2^{29} 229 2 30 2^{30} 2300.000011805306457
2 30 2^{30} 230 2 31 2^{31} 2310.0000001610612741

来源 :http://planetmath.org/goodhashtableprimes

  • 78
    点赞
  • 309
    收藏
    觉得还不错? 一键收藏
  • 18
    评论
C++中使用hash算法实现字符串匹配,可以采用Rabin-Karp算法。 该算法的基本思想是:将模式串和文本串都看作是一个进制为d的数,通过哈希函数将它们映射为一个整数。由于哈希函数的映射是唯一的,因此只要哈希值相等,就说明模式串和文本串可能相等。因此在比较之前,先比较它们的哈希值,若相等则说明它们可能相等,再逐个比较字符。 具体实现如下: ```c++ #include <iostream> #include <string> using namespace std; const int BASE = 131; // 哈希函数的基数 const int MOD = 1e9 + 7; // 哈希函数的模数 int Hash(string s) // 哈希函数 { int res = 0; for (int i = 0; i < s.length(); i++) { res = ((long long)res * BASE + s[i]) % MOD; } return res; } int main() { string text, pattern; cin >> text >> pattern; int patternHash = Hash(pattern); // 计算模式串的哈希值 int textHash = 0; int t = 1; for (int i = 0; i < pattern.length(); i++) { textHash = ((long long)textHash * BASE + text[i]) % MOD; // 计算初始文本串的哈希值 t = ((long long)t * BASE) % MOD; } for (int i = 0; i + pattern.length() <= text.length(); i++) { if (textHash == patternHash && text.substr(i, pattern.length()) == pattern) { cout << "Pattern found at index " << i << endl; } if (i + pattern.length() < text.length()) { textHash = ((long long)textHash * BASE - (long long)text[i] * t % MOD + MOD) % MOD; // 更新文本串的哈希值 textHash = ((long long)textHash + text[i + pattern.length()]) % MOD; } } return 0; } ``` 该算法的时间复杂度为O(n+m),其中n为文本串的长度,m为模式串的长度。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 18
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值